/*
 * Copyright (c) 2015, the DeltaSync# project. All Rights Reserved.
 * This project is a C# port of the JDeltaSync project:
 *     http://jdeltasync.googlecode.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//#define DONOTUSE_MimeKit

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security;
using System.Text;
using System.Xml;
using PM.Net.DeltaSync.hu01;

#if !DONOTUSE_MimeKit
using MimeKit;
#endif

namespace PM.Net.DeltaSync {
    public delegate void DeltaSyncMessageProcessor(Stream data);
    
    /**
     * Main class used to communicate with the Windows Live Hotmail service using
     * Microsoft's proprietary DeltaSync protocol. {@link #login(string, string)} 
     * has to be called to obtain a {@link DeltaSyncSession} which can then be used 
     * to query for the folders and messages on the server and to delete messages.
     */
    public class DeltaSyncClient : IDisposable {
        private static string LOGIN_BASE_URI   = "https://login.live.com/RST2.srf";
        private static string LOGIN_USER_AGENT = 
              "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; IDCRL 5.000.819.1; " 
            + "IDCRL-cfg 6.0.11409.0; App wlmail.exe, 14.0.8117.416, " 
            + "{47A6D4CF-5EB0-4B0E-9138-1B3F2DD40981})";
        private static string DS_USER_AGENT    = "WindowsLiveMail/1.0";
        private static string DS_BASE_URI      = "http://mail.services.live.com";
        private static byte[] LINE_SEPARATOR;
        
        private delegate T UriCapturingResponseHandler<T>(HttpResponseMessage response);
        
        #if DEBUG
        public delegate void DeltaSyncMessageProcessorEvent(string messageId, HU01Exception error, Stream hu01Stream);
        public static DeltaSyncMessageProcessorEvent OnHU01Exception;
        
        public static bool DebugerActive {
            get { return false; }
        }
        #endif
    
        static DeltaSyncClient() {
            LINE_SEPARATOR = Encoding.ASCII.GetBytes(Environment.NewLine); // System.getProperty("line.separator").getBytes("ASCII");
        }
    
        private HttpClient      httpClient;
        private CookieContainer httpCoockies;
        private int             maxRedirections = 10;
        private TimeSpan        _sessionTo      = new TimeSpan(0, 5, 0);
        
        /**
         * Creates a new {@link DeltaSyncClient} using a 
         * {@link ThreadSafeClientConnManager} with the default settings.
         */
        public DeltaSyncClient()
            : this(new HttpClientHandler())
        {}
    
        ~DeltaSyncClient() {
            Dispose(false);
        }
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        
        protected virtual void Dispose(bool disposing) {
            if (disposing)
                this.httpClient.Dispose();
        }
           
        /**
         * Creates a new {@link DeltaSyncClient} using the specified 
         * {@link ClientConnectionManager}. 
         * 
         * @param connectionManager the {@link ClientConnectionManager}.
         */
        protected DeltaSyncClient(HttpMessageHandler messageHandler) {
            HttpClientHandler httphandler = messageHandler as HttpClientHandler;
            if (httphandler != null) {
                httpCoockies    = httphandler.CookieContainer;
                maxRedirections = httphandler.AllowAutoRedirect ? httphandler.MaxAutomaticRedirections : 0;
                httphandler.AllowAutoRedirect = false;
            }
            this.httpClient = new HttpClient(messageHandler);
            this.httpClient.DefaultRequestHeaders.Add("User-Agent", LOGIN_USER_AGENT);
            //setConnectionTimeout(5 * 1000);
            //setSoTimeout(60 * 1000);
        }
        
        #region Properties...
        /**
         * Returns the {@link ClientConnectionManager} in use.
         * 
         * @return the {@link ClientConnectionManager}.
         */
        /*public ClientConnectionManager getConnectionManager() {
            return httpClient.getConnectionManager();
        }*/
        
        /**
         * Sets the connection timeout of the {@link HttpClient} instance. See
         * {@link CoreConnectionPNames#CONNECTION_TIMEOUT}.
         * 
         * @param timeout the timeout.
         */
        /*public void setConnectionTimeout(int timeout) {
            HttpConnectionParams.setConnectionTimeout(this.httpClient.getParams(), timeout);
        }*/
        
        /**
         * Sets the socket timeout (SO_TIMEOUT) of the {@link HttpClient} instance. See
         * {@link CoreConnectionPNames#SO_TIMEOUT}.
         * 
         * @param timeout the timeout.
         */
        /*public void setSoTimeout(int timeout) {
            HttpConnectionParams.setSoTimeout(this.httpClient.getParams(), timeout);
        }*/
        
        /**
         * Sets the timeout (SO_TIMEOUT) of the {@link HttpClient} instance.
         * 
         * @param timeout the timeout.
         */
        public TimeSpan Timeout {
            get { return this.httpClient.Timeout;  }
            set { this.httpClient.Timeout = value; }
        }
        
        public TimeSpan SessionTimeout {
            get { return _sessionTo;  }
            set { 
                if (value <= TimeSpan.Zero)
                    throw new ArgumentException("Invalid timeout value specified");
                _sessionTo = value; 
            }
        }
        #endregion
        
        /**
         * Logs in using the specified username and password. Returns a 
         * {@link DeltaSyncSession} object on successful authentication. 
         * 
         * @param username the username.
         * @param password the password.
         * @return the session.
         * @throws AuthenticationException if authentication fails.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         */
        public DeltaSyncSession Login(string username, string password) 
        {
            if (username == null)
                throw new ArgumentNullException("username");
            if (password == null)
                throw new ArgumentNullException("password");
            return Login(new NetworkCredential(username, password));
        }

        public DeltaSyncSession Login(string username, SecureString password)
        {
            if (username == null)
                throw new ArgumentNullException("username");
            if (password == null)
                throw new ArgumentNullException("password");
            return Login(new NetworkCredential(username, password));
        }
        
        private static string GetFullUserName(NetworkCredential credential) {
            if (credential == null)
                throw new ArgumentNullException("Credential");
            return GetFullUserName(credential.UserName, credential.Domain);
        }
        private static string GetFullUserName(string UserName, string Domain) {
            if (string.IsNullOrEmpty(UserName))
                throw new ArgumentNullException("UserName");
            if (string.IsNullOrEmpty(Domain) || UserName.IndexOf('@') >= 0)
                return UserName;
            return string.Format("{0}@{1}", UserName, Domain);
        }
        
        protected DeltaSyncSession Login(NetworkCredential credential) 
        {
            if (credential == null)
                throw new ArgumentNullException("Credentials");
            
            DateTime created = XmlUtil.TruncateMillis(DateTime.UtcNow);
            DateTime expires = created + SessionTimeout; //new TimeSpan(0, 5, 0);
            
            XmlDocument request    = XmlUtil.Parse(System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("login-request.xml"));
            XmlElement  elSecurity = XmlUtil.getElement(request, "/s:Envelope/s:Header/wsse:Security");
            XmlUtil.setTextContent(elSecurity, "wsse:UsernameToken/wsse:Username", GetFullUserName(credential));
            XmlUtil.setTextContent(elSecurity, "wsse:UsernameToken/wsse:Password", credential.Password);
            XmlUtil.setTextContent(elSecurity, "wsu:Timestamp/wsu:Created", XmlUtil.FormatDateTime(created));
            XmlUtil.setTextContent(elSecurity, "wsu:Timestamp/wsu:Expires", XmlUtil.FormatDateTime(expires));
            
            #if DEBUG
            if (DebugerActive)
                System.Diagnostics.Debug.WriteLine(string.Format("Sending login request:\n{0}", 
                    XmlUtil.ToString(request, false).Replace("password", "******")));
            #endif
            XmlDocument response = post<XmlDocument>(new Uri(LOGIN_BASE_URI), LOGIN_USER_AGENT, "application/soap+xml", request,
                (HttpResponseMessage respMsg) => XmlUtil.Parse(respMsg.Content.ReadAsStreamAsync().Result));
            #if DEBUG
            if (DebugerActive)
                System.Diagnostics.Debug.WriteLine(string.Format("Received login response:\n{0}", XmlUtil.ToString(response, false)));
            #endif
                
            if (XmlUtil.hasElement(response, "/s:Envelope/s:Body/s:Fault"))
                throw new UnauthorizedAccessException(XmlUtil.getTextContent(response, "/s:Envelope/s:Body/s:Fault/s:Reason/s:Text")); // AuthenticationException
            
            string ticket = XmlUtil.getTextContent(response, "/s:Envelope/s:Body/wst:RequestSecurityTokenResponseCollection/" 
                    + "wst:RequestSecurityTokenResponse/wst:RequestedSecurityToken/wsse:BinarySecurityToken");
            if (ticket == null) {
                string flowUrl = XmlUtil.getTextContent(response, "/s:Envelope/s:Body/wst:RequestSecurityTokenResponseCollection/" 
                        + "wst:RequestSecurityTokenResponse/psf:pp/psf:flowurl");
                string requestStatus = XmlUtil.getTextContent(response, "/s:Envelope/s:Body/wst:RequestSecurityTokenResponseCollection/" 
                        + "wst:RequestSecurityTokenResponse/psf:pp/psf:reqstatus");
                string errorStatus = XmlUtil.getTextContent(response, "/s:Envelope/s:Body/wst:RequestSecurityTokenResponseCollection/" 
                        + "wst:RequestSecurityTokenResponse/psf:pp/psf:errorstatus");
                if (flowUrl != null || requestStatus != null || errorStatus != null)
                    throw new AuthenticationException(flowUrl, requestStatus, errorStatus);
                throw new AuthenticationException("Uknown authentication failure");
            }
            
            DeltaSyncSession session = new DeltaSyncSession(credential, ticket);
            session.BaseUri = new Uri(DS_BASE_URI);
            return session;
        }
        
        /**
         * Renews the specified session and returns a new session. The old session
         * object must be discarded. This should be called when a {@link SessionExpiredException}
         * has been thrown indicating that a session has expired.
         * 
         * @param session the old session.
         * @return the new session.
         * @throws AuthenticationException if authentication fails.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         */
        public DeltaSyncSession Renew(DeltaSyncSession session)
        {
            return Login(session.Credential.UserName, session.Credential.SecurePassword);
        }
        
        /**
         * Downloads the HU01 compressed content of the message with the specified 
         * id and writes it to the specified {@link OutputStream}.
         * 
         * @param session the session.
         * @param messageId the id of the message to download.
         * @param out the stream to write the HU01 compressed message content to.
         * @throws SessionExpiredException if the session has expired.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         */
        public bool DownloadRawMessageContent(DeltaSyncSession session, string messageId, Stream output) 
        {
            return DownloadMessageContent(session, messageId, output, true);
        }
        
        /**
         * Downloads the content of the message with the specified id and writes it 
         * to the specified {@link OutputStream}.
         * 
         * @param session the session.
         * @param messageId the id of the message to download.
         * @param out the stream to write the message content to.
         * @throws SessionExpiredException if the session has expired.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         */
        public bool DownloadMessageContent(DeltaSyncSession session, string messageId, Stream output) 
        {
            return DownloadMessageContent(session, messageId, output, false);
        }
        
        private bool DownloadMessageContent(DeltaSyncSession session, string messageId, Stream output, bool raw) {
            bool success = false;
            DownloadMessageContent(session, messageId, (Stream stream) => { stream.CopyTo(output); success = true; }, raw);
            return success;
        }
        
        public void DownloadMessageContent(DeltaSyncSession session, string messageId, DeltaSyncMessageProcessor processor, bool raw = false) {
            string request = 
                  "<ItemOperations xmlns=\"ItemOperations:\" xmlns:A=\"HMMAIL:\">"
                +   "<Fetch>"
                +     "<Class>Email</Class>"
                +     "<A:ServerId>" + messageId + "</A:ServerId>"
                +     "<A:Compression>hm-compression</A:Compression>"
                +     "<A:ResponseContentType>mtom</A:ResponseContentType>"
                +   "</Fetch>"
                + "</ItemOperations>";
         
            XmlDocument response = call(session, "ItemOperations", request, 
                (HttpResponseMessage respMsg) => {
                    var contentType = respMsg.Content.Headers.ContentType;
                    if (contentType == null || contentType.MediaType != "application/xop+xml") {
                        if (contentType != null && contentType.MediaType == "text/xml") {
                            // If we receive a text/xml response it means an error has occurred
                            return XmlUtil.Parse(respMsg.Content.ReadAsStreamAsync().Result);
                        }
                        throw new DeltaSyncException("Unexpected Content-Type received: " + contentType.MediaType);
                    }
                    
                    object result = null;
                    #if !DONOTUSE_MimeKit
                    var parser = new MimeParser(respMsg.Content.ReadAsStreamAsync().Result);
                    foreach (MimePart part in parser.ParseMessage().BodyParts) {
                        var partContentType = part.ContentType;
                        if (partContentType == null)
                            continue;
                        else if (partContentType.MimeType == "application/xop+xml") {
                            try {
                                using (var instm = part.ContentObject.Open())
                                    result = XmlUtil.Parse(instm);
                            } catch (Exception ex) {
                                result = ex;
                            }
                        }
                        else if (partContentType.MimeType == "application/octet-stream") {
                            try {
                                using (var instm = part.ContentObject.Open())
                                       processor(raw ? instm : new HU01DecompressorStream(instm, false));
                            } catch (HU01Exception ex) {
                                #if DEBUG
                                if (OnHU01Exception != null) {
                                    try {
                                        using (var instm = part.ContentObject.Open())
                                            OnHU01Exception(messageId, ex, instm);
                                    } catch {
                                        // ignore errors...
                                    }
                                }
                                #endif
                                throw ex;
                            }
                        }
                    }
                    #else
                    using (var mimecontent = PM.Net.Http.MimeParser.Parse(respMsg.Content.ReadAsStreamAsync().Result)) {
                        foreach (HttpContent content in PM.Net.Http.MimeParser.GetContents(mimecontent)) {
                            contentType = content.Headers.ContentType;
                            if (contentType == null)
                                continue;
                            else if (contentType.MediaType == "application/xop+xml") {
                                try {
                                    using (var instm = content.ReadAsStreamAsync().Result)
                                        result = XmlUtil.Parse(content.ReadAsStreamAsync().Result);
                                } catch (Exception ex) {
                                    result = ex;
                                }
                            }
                            else if (contentType.MediaType == "application/octet-stream") {
                                try {
                                    using (var instm = content.ReadAsStreamAsync().Result)
                                        processor(raw ? instm : new HU01DecompressorStream(instm, false));
                                } catch (HU01Exception ex) {
                                    #if DEBUG
                                    if (OnHU01Exception != null) {
                                        try {
                                            using (var instm = content.ReadAsStreamAsync().Result)
                                                OnHU01Exception(messageId, ex, instm);
                                        } catch {
                                            // ignore errors...
                                        }
                                    }
                                    #endif
                                    throw ex;
                                }
                            }
                        }
                    }
                    #endif
                    
                    if (result is Exception)
                        throw (Exception)result;
                    return (XmlDocument)result;
                }
            );
            
            #if DEBUG
            if (DebugerActive) {
                System.Diagnostics.Debug.WriteLine(string.Format("Received ItemOperations response: {0}", XmlUtil.ToString(response, false)));
            }
            #endif
            checkStatus(response);
            // No general error in the response. Check for a specific <Fetch> error.
            XmlElement elStatus = XmlUtil.getElement(response, 
                    "/itemop:ItemOperations/itemop:Responses/itemop:Fetch/itemop:Status");
            if (elStatus == null)
                throw new DeltaSyncException("No <Status> element found in <Fetch> response: " + XmlUtil.ToString(response, true));            
            int    code;
            string codeStatus = XmlUtil.getTextContent(elStatus);
            if (codeStatus == null)
                code = -1;
            else if (!int.TryParse(codeStatus, out code))
                code = -1;
            if (code == 4403)
                throw new NoSuchMessageException(messageId);
            else if (code != 1) {
                throw new UnrecognizedErrorCodeException(code, 
                        "Unrecognized error code in response for <Fetch> request. Response was: "
                        + XmlUtil.ToString(response, true));
            }
        }
    
        public SyncResponse sync(DeltaSyncSession session, SyncRequest syncRequest)
        {
            StringBuilder request = new StringBuilder("<Sync xmlns=\"AirSync:\"><Collections>");
            foreach (SyncRequest.Collection collection in syncRequest.Collections) {
                request.Append("<Collection>");
                request.Append("<Class>").Append(collection.Clazz).Append("</Class>");
                if (collection.CollectionId != null) {
                    request.Append("<CollectionId>").Append(collection.CollectionId).Append("</CollectionId>");                
                }
                request.Append("<SyncKey>").Append(collection.SyncKey).Append("</SyncKey>");
                if (collection.IsGetChanges) {
                    request.Append("<GetChanges/>");
                }
                if (collection.WindowSize > 0) {
                    request.Append("<WindowSize>").Append(collection.WindowSize).Append("</WindowSize>");
                }
                if (collection.Commands.Count > 0) {
                    request.Append("<Commands>");
                    foreach (Command command in collection.Commands) {
                        switch (collection.Clazz) {
                        case Clazz.Email:
                            request.Append("<Delete>").Append("<ServerId>");
                            request.Append(((EmailDeleteCommand)  command).Id);
                            request.Append("</ServerId>").Append("</Delete>");
                            break;
                        case Clazz.Folder:
                            request.Append("<Delete>").Append("<ServerId>");
                            request.Append(((FolderDeleteCommand) command).Id);
                            request.Append("</ServerId>").Append("</Delete>");
                            break;
                        default:
                            throw new NotSupportedException("Operation not supported exception: clazz=" + collection.Clazz);
                        }
                    }
                    request.Append("</Commands>");
                }
                request.Append("</Collection>");
            }
            request.Append("</Collections></Sync>");
            
            XmlDocument response = sync(session, request.ToString());
    
            List<SyncResponse.Collection> collections = new List<SyncResponse.Collection>();
            foreach (XmlElement elCollection in XmlUtil.getElements(response, "//airsync:Collection")) {
                string        syncKey       = XmlUtil.getTextContent(elCollection, "airsync:SyncKey");
                Clazz         clazz         = (Clazz)Enum.Parse(typeof(Clazz), XmlUtil.getTextContent(elCollection, "airsync:Class"));
                int           status        = XmlUtil.ParseInt(XmlUtil.getTextContent(elCollection, "airsync:Status"));
                bool          moreAvailable = XmlUtil.hasElement(elCollection, "airsync:MoreAvailable");
                List<Command> commands      = new List<Command>();
                
                switch (clazz) {
                case Clazz.Email:
                    foreach (XmlElement elAdd in XmlUtil.getElements(elCollection, "airsync:Commands/airsync:Add")) {
                        try {
                            string     id             = XmlUtil.getTextContent(elAdd, "airsync:ServerId");
                            string     folderId       = XmlUtil.getTextContent(elAdd, "hmmail:FolderId");
                            XmlElement elAppData      = XmlUtil.getElement    (elAdd, "airsync:ApplicationData");
                            long       size           = XmlUtil.ParseLong    (XmlUtil.getTextContent(elAppData, "hmmail:Size"));
                            bool       read           = XmlUtil.ParseInt     (XmlUtil.getTextContent(elAppData, "email:Read"))            == 1;
                            bool       hasAttachments = XmlUtil.ParseInt     (XmlUtil.getTextContent(elAppData, "hmmail:HasAttachments")) == 1;
                            DateTime   dateReceived   = XmlUtil.ParseDateTime(XmlUtil.getTextContent(elAppData, "email:DateReceived"));
                            string     subject        = XmlUtil.getTextContent(elAppData, "email:Subject");
                            string     from           = XmlUtil.getTextContent(elAppData, "email:From");
                            commands.Add(new EmailAddCommand(id, folderId, dateReceived, size, read, subject, from, hasAttachments));
                        } catch (FormatException ex) {
                            throw new DeltaSyncException(ex.Message, ex);
                        }
                    }
                        
                    // TODO: AirSync:Change
                    foreach (XmlElement elDelete in XmlUtil.getElements(elCollection, "airsync:Commands/airsync:Delete")) {
                        string id = XmlUtil.getTextContent(elDelete, "airsync:ServerId");
                        commands.Add(new EmailDeleteCommand(id));
                    }
                    break;
                case Clazz.Folder:
                    foreach (XmlElement elAdd in XmlUtil.getElements(elCollection, "airsync:Commands/airsync:Add")) {
                        string id = XmlUtil.getTextContent(elAdd, "airsync:ServerId");
                        string displayName = XmlUtil.getTextContent(elAdd, "airsync:ApplicationData/hmfolder:DisplayName");
                        commands.Add(new FolderAddCommand(id, displayName));
                    }
                    
                    foreach (XmlElement elDelete in XmlUtil.getElements(elCollection, "airsync:Commands/airsync:Delete")) {
                        string id = XmlUtil.getTextContent(elDelete, "airsync:ServerId");
                        commands.Add(new FolderDeleteCommand(id));
                    }
                    break;
                }
    
                List<SyncResponse.Collection.Response> responses = new List<SyncResponse.Collection.Response>();
                
                // TODO: Support for other types of responses
                foreach (XmlElement elDelete in XmlUtil.getElements(elCollection, "airsync:Responses/airsync:Delete")) {
                    string id           = XmlUtil.getTextContent(elDelete, "airsync:ServerId");
                    int    deleteStatus = int.Parse(XmlUtil.getTextContent(elDelete, "airsync:Status"));
                    responses.Add(new SyncResponse.Collection.EmailDeleteResponse(id, deleteStatus));
                }            
                
                collections.Add(new SyncResponse.Collection(syncKey, clazz, status, commands, moreAvailable, responses));
            }
            
            return new SyncResponse(collections);
        }
        
        private XmlDocument sync(DeltaSyncSession session, string request) {
            return call(session, "Sync", request, 
                (HttpResponseMessage response) => {
                    XmlDocument doc = XmlUtil.Parse(response.Content.ReadAsStreamAsync().Result);
                    #if DEBUG
                    if (DebugerActive) {
                        System.Diagnostics.Debug.WriteLine(string.Format("Received Sync response: {0}", XmlUtil.ToString(doc, false)));
                    }
                    #endif
                    checkStatus(doc);
                    return doc;
                }
            );
        }
        
        #region Transport...
        private T call<T>(DeltaSyncSession session, string cmd, string request, UriCapturingResponseHandler<T> handler) {
            #if DEBUG
            if (DebugerActive) {
                try {
                    XmlDocument document = XmlUtil.ParseXml(request);
                    System.Diagnostics.Debug.WriteLine(string.Format("Sending {0} request:\n{1}", cmd, XmlUtil.ToString(document, false)));
                } catch {
                    System.Diagnostics.Debug.WriteLine(string.Format("Sending {0} request:\n{1}", cmd, request));
                }
            }
            #endif
            
            return post(new Uri(session.BaseUri, "/DeltaSync_v2.0.0/" + cmd + ".aspx?" + session.Ticket),
                DS_USER_AGENT, "text/xml", request, handler, session);
        }
        
        private void checkStatus(XmlDocument doc) {
            XmlElement status = XmlUtil.getElement(doc.DocumentElement, "*:Status");
            if (status == null) {
                // All responses should have a <Status> element
                throw new DeltaSyncException("No <Status> element found in response: " + XmlUtil.ToString(doc, true));            
            }
            int code = XmlUtil.ParseInt(XmlUtil.getTextContent(status));
            if (code != 1) {
                string message = XmlUtil.getTextContent(doc.DocumentElement, "*:Fault/*:Faultstring");
                if (message == null) {
                    message = "No Faultstring provided in response. Response was: " + XmlUtil.ToString(doc, true);
                }
                switch (code) {
                case 3204:
                    // Authentication failure. We assume this means that the session has expired.
                    throw new SessionExpiredException(message);
                case 4102:
                    // The server failed to understand the request due to a syntax error or an error in the parameters.
                    throw new BadRequestException(message);
                case 4104:
                    // Invalid sync key.
                    throw new InvalidSyncKeyException(message);
                case 4402:
                    throw new NoSuchFolderException(message);
                default:
                    throw new UnrecognizedErrorCodeException(code, message);
                }
            }
        }
        
        private T post<T>(Uri uri, string userAgent, string contentType, XmlDocument doc,
                UriCapturingResponseHandler<T> handler, DeltaSyncSession session = null) {
            return post(uri, userAgent, contentType, XmlUtil.ToByteArray(doc), handler, session);
        }
        
        private T post<T>(Uri uri, string userAgent, string contentType, string s, 
                UriCapturingResponseHandler<T> handler, DeltaSyncSession session = null) {
            return post(uri, userAgent, contentType, Encoding.UTF8.GetBytes(s), handler, session);
        }
        
        private T post<T>(Uri uri, string userAgent, string contentType, 
                byte[] data, UriCapturingResponseHandler<T> handler, DeltaSyncSession session = null) {
            lock(this.httpClient) {
                this.httpClient.DefaultRequestHeaders.Remove("User-Agent");
                this.httpClient.DefaultRequestHeaders.Add   ("User-Agent", userAgent);
                
                for (int redirectsCount = maxRedirections; true; redirectsCount--) {
                    ByteArrayContent content = new ByteArrayContent(data);
                    content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
                    using (HttpResponseMessage response = this.httpClient.PostAsync(uri, content).Result) {
                        Uri redirectUri;
                        if (isRedirect(response, out redirectUri)) {
                            if (redirectsCount <= 0 || redirectUri == null)
                                throw new HttpException(response.StatusCode, response.ReasonPhrase);
                            if (!redirectUri.IsAbsoluteUri)
                                redirectUri = new Uri(uri, redirectUri);
                            uri = redirectUri; // continue loop...
                        }
                        else if (response.StatusCode != HttpStatusCode.OK)
                            throw new HttpException(response.StatusCode, response.ReasonPhrase);
                        else {
                            if (session != null) {
                                Uri requri = response.RequestMessage.RequestUri;
                                session.BaseUri = new Uri(requri.Scheme + "://" + requri.Host);
                            }
                            return handler(response);
                        }
                    }
                }
            }
        }

        /**
         * Modified version of {@link DefaultRedirectStrategy#isRedirected(HttpRequest, HttpResponse, HttpContext)}
         * which also returns <code>true</code> for POSTs being redirected, not only for GETs and HEADs.
         */
        private bool isRedirect(HttpResponseMessage response, out Uri location) {
            switch (response.StatusCode) {
            case HttpStatusCode.Found:
            case HttpStatusCode.MovedPermanently:
            case HttpStatusCode.TemporaryRedirect:
            case HttpStatusCode.SeeOther:
                location = response.Headers.Location;
                return true;
            }
            location = null;
            return false;
        }
        #endregion
    }
}